<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>3D Tetris Adventure</title>
  <style>
    body {
      margin: 0;
      overflow: hidden;
      background-color: #e0f7fa;
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
      font-family: 'Comic Sans MS', cursive, sans-serif;
    }

    #game-container {
      position: relative;
    }

    #score {
      position: absolute;
      top: 10px;
      left: 10px;
      color: #00695c;
      font-size: 24px;
      text-shadow: 1px 1px #b2ebf2;
    }

    #instructions {
      position: absolute;
      bottom: 10px;
      width: 100%;
      text-align: center;
      color: #004d40;
      font-size: 14px;
    }

    canvas {
      display: block;
      border: 5px solid #80cbc4;
      box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15);
    }

    h1 {
      position: absolute;
      top: 20px;
      width: 100%;
      text-align: center;
      color: #00695c;
      text-shadow: 2px 2px #b2ebf2;
      z-index: 10;
      pointer-events: none;
    }
  </style>
</head>

<body>
  <h1>🎉 3D Cartoon Tetris 🎉</h1>
  <div id="game-container">
    <div id="score">Score: 0</div>
    <div id="instructions">Use Arrow Keys to Move, A/D to Rotate Z, W/S to Rotate X, Q/E to Rotate Y. Space to Drop.
    </div>
    <canvas id="gameCanvas"></canvas>
  </div>

  <script type="importmap">
        {
            "imports": {
                "three": "https://unpkg.com/three@0.163.0/build/three.module.js",
                "three/addons/": "https://unpkg.com/three@0.163.0/examples/jsm/"
            }
        }
    </script>
  <script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // Optional: for debugging/viewing

    let scene, camera, renderer, controls;
    let gameBoard, currentPiece;
    let score = 0;
    const scoreElement = document.getElementById('score');

    const boardWidth = 10;
    const boardHeight = 20;
    const boardDepth = 10; // Added depth for 3D
    const cellSize = 1; // Size of each cube unit

    const colors = [
      0xff0000, // Red
      0x00ff00, // Lime
      0x0000ff, // Blue
      0xffff00, // Yellow
      0xff00ff, // Magenta
      0x00ffff, // Cyan
      0xffa500, // Orange
    ];

    // Basic Tetromino shapes in 3D (relative coordinates from pivot)
    // Using a simplified representation for now
    const shapes = [
      // I shape
      { color: 0x00ffff, blocks: [[0, 0, 0], [0, 1, 0], [0, 2, 0], [0, 3, 0]] },
      // O shape
      { color: 0xffff00, blocks: [[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0]] },
      // T shape
      { color: 0xff00ff, blocks: [[0, 0, 0], [1, 0, 0], [2, 0, 0], [1, 1, 0]] },
      // L shape
      { color: 0xffa500, blocks: [[0, 0, 0], [0, 1, 0], [0, 2, 0], [1, 2, 0]] },
      // J shape
      { color: 0x0000ff, blocks: [[1, 0, 0], [1, 1, 0], [1, 2, 0], [0, 2, 0]] },
      // S shape
      { color: 0x00ff00, blocks: [[1, 0, 0], [2, 0, 0], [0, 1, 0], [1, 1, 0]] },
      // Z shape
      { color: 0xff0000, blocks: [[0, 0, 0], [1, 0, 0], [1, 1, 0], [2, 1, 0]] },
    ];

    // Game state
    let gameLoopInterval;
    let fallSpeed = 1000; // ms per step down
    let isGameOver = false;

    function init () {
      // Scene
      scene = new THREE.Scene();
      scene.background = new THREE.Color(0xe0f7fa);

      // Camera
      const aspect = window.innerWidth / window.innerHeight;
      // Adjusted camera position for better 3D view
      camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 1000);
      camera.position.set(boardWidth * cellSize / 2, boardHeight * cellSize * 0.75, boardDepth * cellSize * 2.5); // Positioned to view the board
      camera.lookAt(boardWidth * cellSize / 2, boardHeight * cellSize / 2, boardDepth * cellSize / 2);

      // Renderer
      const canvas = document.getElementById('gameCanvas');
      renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
      // Adjust canvas size dynamically or set fixed size
      const container = document.getElementById('game-container');
      // Let's set a fixed size for simplicity, adjust as needed
      const canvasWidth = 400;
      const canvasHeight = 600;
      renderer.setSize(canvasWidth, canvasHeight);
      camera.aspect = canvasWidth / canvasHeight;
      camera.updateProjectionMatrix();


      // Lighting
      const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
      scene.add(ambientLight);
      const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
      directionalLight.position.set(5, 10, 7.5);
      scene.add(directionalLight);

      // Optional OrbitControls for debugging view
      // controls = new OrbitControls(camera, renderer.domElement);
      // controls.target.set(boardWidth * cellSize / 2, boardHeight * cellSize / 2, boardDepth * cellSize / 2);
      // controls.update();

      // Create Game Board representation (logical, not visual yet)
      gameBoard = Array(boardWidth).fill(0).map(() =>
        Array(boardHeight).fill(0).map(() =>
          Array(boardDepth).fill(null) // null means empty, otherwise stores the block object
        )
      );

      // Create visual boundary (optional)
      createBoardBoundary();

      // Start Game
      spawnNewPiece();
      startGameLoop();
      setupControls();

      // Start rendering loop
      animate();
    }

    function createBoardBoundary () {
      const geometry = new THREE.BoxGeometry(boardWidth * cellSize, boardHeight * cellSize, boardDepth * cellSize);
      const material = new THREE.MeshBasicMaterial({ color: 0xaaaaaa, wireframe: true, transparent: true, opacity: 0.3 });
      const wireframe = new THREE.Mesh(geometry, material);
      // Center the wireframe visually
      wireframe.position.set(
        (boardWidth * cellSize / 2) - cellSize / 2,
        (boardHeight * cellSize / 2) - cellSize / 2,
        (boardDepth * cellSize / 2) - cellSize / 2
      );
      scene.add(wireframe);
    }


    function createBlock (color) {
      const geometry = new THREE.BoxGeometry(cellSize, cellSize, cellSize);
      // Add edges for definition
      const edges = new THREE.EdgesGeometry(geometry);
      const lineMaterial = new THREE.LineBasicMaterial({ color: 0x333333, linewidth: 2 }); // Darker edges
      const edgeLines = new THREE.LineSegments(edges, lineMaterial);

      // Main block material with some cartoonish feel
      const material = new THREE.MeshStandardMaterial({
        color: color,
        roughness: 0.7,
        metalness: 0.1,
      });
      const cube = new THREE.Mesh(geometry, material);
      cube.add(edgeLines); // Add edges to the cube mesh
      return cube;
    }

    function spawnNewPiece () {
      if (isGameOver) return;

      const shapeIndex = Math.floor(Math.random() * shapes.length);
      const shapeInfo = shapes[shapeIndex];
      const pieceGroup = new THREE.Group();
      const blocks = []; // Store individual block meshes

      shapeInfo.blocks.forEach(offset => {
        const block = createBlock(shapeInfo.color);
        block.position.set(offset[0] * cellSize, offset[1] * cellSize, offset[2] * cellSize);
        pieceGroup.add(block);
        blocks.push(block); // Keep track of block meshes
      });

      // Initial position: Center X/Z, Top Y
      const startX = Math.floor(boardWidth / 2) - 1;
      const startY = boardHeight - 1; // Start at the very top
      const startZ = Math.floor(boardDepth / 2) - 1;

      pieceGroup.position.set(
        startX * cellSize,
        startY * cellSize,
        startZ * cellSize
      );

      // Store logical position (using the group's position)
      pieceGroup.userData = {
        shape: shapeInfo,
        blocks: blocks, // Store block meshes for individual access if needed
        gridPos: { x: startX, y: startY, z: startZ },
        shapeIndex: shapeIndex // Store shape index if needed later
      };

      currentPiece = pieceGroup;

      // Check for immediate collision (Game Over condition)
      if (checkCollision(currentPiece.userData.gridPos.x, currentPiece.userData.gridPos.y, currentPiece.userData.gridPos.z, currentPiece)) {
        console.log("Game Over!");
        isGameOver = true;
        clearInterval(gameLoopInterval);
        // Display Game Over message
        const gameOverText = document.createElement('div');
        gameOverText.style.position = 'absolute';
        gameOverText.style.top = '50%';
        gameOverText.style.left = '50%';
        gameOverText.style.transform = 'translate(-50%, -50%)';
        gameOverText.style.color = 'red';
        gameOverText.style.fontSize = '40px';
        gameOverText.style.fontWeight = 'bold';
        gameOverText.textContent = 'GAME OVER';
        document.getElementById('game-container').appendChild(gameOverText);
        return; // Stop spawning
      }

      scene.add(currentPiece);
    }


    function movePiece (dx, dy, dz) {
      if (isGameOver || !currentPiece) return false;

      const currentPos = currentPiece.userData.gridPos;
      const nextX = currentPos.x + dx;
      const nextY = currentPos.y + dy;
      const nextZ = currentPos.z + dz;

      if (!checkCollision(nextX, nextY, nextZ, currentPiece)) {
        currentPiece.position.x += dx * cellSize;
        currentPiece.position.y += dy * cellSize;
        currentPiece.position.z += dz * cellSize;
        currentPiece.userData.gridPos = { x: nextX, y: nextY, z: nextZ };
        return true; // Move successful
      } else if (dy < 0) { // Collision while moving down
        freezePiece();
        return false; // Could not move down, piece is frozen
      }
      return false; // Collision horizontally or upwards (shouldn't happen upwards)
    }

    function rotatePiece (axis, angle) {
      if (isGameOver || !currentPiece) return;

      // Clone the piece to test rotation
      const testPiece = currentPiece.clone();
      testPiece.userData = { gridPos: { ...currentPiece.userData.gridPos } }; // Copy grid position

      // Apply rotation around the group's origin (which is relative to the first block usually)
      const center = new THREE.Vector3(0, 0, 0); // Rotation pivot relative to group origin
      const radAngle = THREE.MathUtils.degToRad(angle);

      if (axis === 'x') testPiece.rotateOnWorldAxis(new THREE.Vector3(1, 0, 0), radAngle);
      else if (axis === 'y') testPiece.rotateOnWorldAxis(new THREE.Vector3(0, 1, 0), radAngle);
      else if (axis === 'z') testPiece.rotateOnWorldAxis(new THREE.Vector3(0, 0, 1), radAngle);
      else return; // Invalid axis

      // Need a more robust collision check after rotation
      // This simple check might not be enough due to complex rotations
      if (!checkCollisionAfterRotation(testPiece)) {
        // Apply actual rotation
        if (axis === 'x') currentPiece.rotateOnWorldAxis(new THREE.Vector3(1, 0, 0), radAngle);
        else if (axis === 'y') currentPiece.rotateOnWorldAxis(new THREE.Vector3(0, 1, 0), radAngle);
        else if (axis === 'z') currentPiece.rotateOnWorldAxis(new THREE.Vector3(0, 0, 1), radAngle);
      } else {
        console.log("Rotation blocked");
        // Optional: Implement wall kick logic here
      }
    }

    // More complex collision check needed for rotation
    function checkCollisionAfterRotation (piece) {
      const matrix = new THREE.Matrix4();
      piece.updateMatrixWorld(); // Ensure world matrix is up to date
      matrix.copy(piece.matrixWorld);

      for (const block of piece.children) {
        if (!block.isMesh) continue; // Skip non-mesh children like edges

        const blockWorldPos = new THREE.Vector3();
        block.getWorldPosition(blockWorldPos); // Get world position of the block

        // Convert world position to grid coordinates
        const gridX = Math.round(blockWorldPos.x / cellSize);
        const gridY = Math.round(blockWorldPos.y / cellSize);
        const gridZ = Math.round(blockWorldPos.z / cellSize);

        // Check boundaries
        if (gridX < 0 || gridX >= boardWidth ||
          gridY < 0 || // No need to check >= boardHeight during rotation check usually
          gridZ < 0 || gridZ >= boardDepth) {
          return true; // Collision with boundary
        }

        // Check collision with frozen blocks
        if (gridY < boardHeight && gameBoard[gridX] && gameBoard[gridX][gridY] && gameBoard[gridX][gridY][gridZ]) {
          return true; // Collision with another block
        }
      }
      return false; // No collision detected
    }


    function checkCollision (baseX, baseY, baseZ, piece) {
      const matrix = new THREE.Matrix4();
      // Simulate the piece's world matrix at the potential new position
      matrix.makeRotationFromQuaternion(piece.quaternion);
      matrix.setPosition(baseX * cellSize, baseY * cellSize, baseZ * cellSize);


      for (const block of piece.children) {
        if (!block.isMesh) continue; // Skip edges etc.

        // Calculate the potential world position of the block's center
        const localPos = block.position.clone();
        const worldPos = localPos.applyMatrix4(matrix);

        // Convert world position to grid coordinates (round carefully)
        const gridX = Math.round(worldPos.x / cellSize);
        const gridY = Math.round(worldPos.y / cellSize);
        const gridZ = Math.round(worldPos.z / cellSize);


        // 1. Check boundaries
        if (gridX < 0 || gridX >= boardWidth ||
          gridY < 0 || // Check bottom boundary
          gridZ < 0 || gridZ >= boardDepth) {
          // console.log(`Boundary Collision: Grid(${gridX}, ${gridY}, ${gridZ}) World(${worldPos.x.toFixed(2)}, ${worldPos.y.toFixed(2)}, ${worldPos.z.toFixed(2)})`);
          return true;
        }

        // 2. Check against frozen blocks (only if within board height)
        // Make sure indices are valid before accessing gameBoard
        if (gridY < boardHeight &&
          gameBoard[gridX] !== undefined &&
          gameBoard[gridX][gridY] !== undefined &&
          gameBoard[gridX][gridY][gridZ] !== null) {
          // console.log(`Block Collision: Grid(${gridX}, ${gridY}, ${gridZ})`);
          return true; // Collision with a frozen block
        }
      }
      return false; // No collision
    }


    function freezePiece () {
      if (!currentPiece) return;

      const matrix = new THREE.Matrix4();
      currentPiece.updateMatrixWorld(); // Ensure world matrix is up to date
      matrix.copy(currentPiece.matrixWorld);

      currentPiece.children.forEach(block => {
        if (!block.isMesh) return; // Skip non-mesh children

        const blockWorldPos = new THREE.Vector3();
        block.getWorldPosition(blockWorldPos); // Get world position

        // Convert world position to grid coordinates
        const gridX = Math.round(blockWorldPos.x / cellSize);
        const gridY = Math.round(blockWorldPos.y / cellSize);
        const gridZ = Math.round(blockWorldPos.z / cellSize);

        // Important: Check if the calculated grid position is valid
        if (gridX >= 0 && gridX < boardWidth &&
          gridY >= 0 && gridY < boardHeight &&
          gridZ >= 0 && gridZ < boardDepth) {

          // Detach block from group and add directly to scene
          const newBlock = block.clone(); // Clone mesh and material
          newBlock.position.copy(blockWorldPos); // Set world position
          newBlock.rotation.setFromRotationMatrix(matrix); // Apply group rotation
          scene.add(newBlock);

          // Add reference to the gameBoard
          gameBoard[gridX][gridY][gridZ] = newBlock; // Store the mesh itself

        } else {
          console.warn("Attempted to freeze block outside bounds:", gridX, gridY, gridZ);
        }
      });

      // Remove the group (original falling piece) from the scene
      scene.remove(currentPiece);
      currentPiece = null; // Clear current piece reference

      clearCompletedLayers();
      spawnNewPiece(); // Spawn the next piece
    }


    function clearCompletedLayers () {
      let layersCleared = 0;
      for (let y = 0; y < boardHeight; y++) {
        let isLayerComplete = true;
        for (let x = 0; x < boardWidth; x++) {
          for (let z = 0; z < boardDepth; z++) {
            if (!gameBoard[x][y][z]) {
              isLayerComplete = false;
              break;
            }
          }
          if (!isLayerComplete) break;
        }

        if (isLayerComplete) {
          layersCleared++;
          console.log(`Clearing layer ${y}`);
          // Remove blocks in the completed layer from the scene and board
          for (let x = 0; x < boardWidth; x++) {
            for (let z = 0; z < boardDepth; z++) {
              if (gameBoard[x][y][z]) {
                scene.remove(gameBoard[x][y][z]);
                gameBoard[x][y][z] = null;
              }
            }
          }

          // Shift down layers above the cleared one
          for (let yy = y; yy < boardHeight - 1; yy++) {
            for (let x = 0; x < boardWidth; x++) {
              for (let z = 0; z < boardDepth; z++) {
                gameBoard[x][yy][z] = gameBoard[x][yy + 1][z];
                if (gameBoard[x][yy][z]) {
                  // Move the visual block down
                  gameBoard[x][yy][z].position.y -= cellSize;
                }
              }
            }
          }
          // Clear the top layer logically
          for (let x = 0; x < boardWidth; x++) {
            for (let z = 0; z < boardDepth; z++) {
              gameBoard[x][boardHeight - 1][z] = null;
            }
          }


          y--; // Re-check the current layer index as it now contains the layer from above
        }
      }

      if (layersCleared > 0) {
        updateScore(layersCleared);
      }
    }

    function updateScore (layersCleared) {
      // Simple scoring: 100 points per layer, bonus for multiple layers
      score += layersCleared * 100 * layersCleared; // e.g., 1 layer = 100, 2 = 400, 3 = 900
      scoreElement.textContent = `Score: ${score}`;
      console.log(`Score: ${score}`);

      // Optional: Increase speed based on score or layers cleared
      // fallSpeed = Math.max(200, 1000 - Math.floor(score / 1000) * 50);
      // clearInterval(gameLoopInterval);
      // startGameLoop();
    }


    function dropPiece () {
      if (isGameOver || !currentPiece) return;
      while (movePiece(0, -1, 0)) {
        // Keep moving down until collision
      }
      // The movePiece function calls freezePiece on collision when moving down
    }


    function setupControls () {
      document.addEventListener('keydown', (event) => {
        if (isGameOver || !currentPiece) return;

        switch (event.key) {
          case 'ArrowLeft': // Move Left (negative X)
            movePiece(-1, 0, 0);
            break;
          case 'ArrowRight': // Move Right (positive X)
            movePiece(1, 0, 0);
            break;
          case 'ArrowUp': // Move Forward (negative Z)
            movePiece(0, 0, -1);
            break;
          case 'ArrowDown': // Move Backwards (positive Z) or Speed up fall
            movePiece(0, 0, 1);
            // Optional: speed up fall temporarily
            // clearInterval(gameLoopInterval);
            // gameLoopInterval = setInterval(gameStep, fallSpeed / 5);
            break;
          case ' ': // Spacebar to drop
            dropPiece();
            break;
          // Rotations (Example: A/D for Z, W/S for X, Q/E for Y)
          case 'a': // Rotate Z anti-clockwise
          case 'A':
            rotatePiece('z', 90);
            break;
          case 'd': // Rotate Z clockwise
          case 'D':
            rotatePiece('z', -90);
            break;
          case 'w': // Rotate X anti-clockwise
          case 'W':
            rotatePiece('x', 90);
            break;
          case 's': // Rotate X clockwise
          case 'S':
            rotatePiece('x', -90);
            break;
          case 'q': // Rotate Y anti-clockwise
          case 'Q':
            rotatePiece('y', 90);
            break;
          case 'e': // Rotate Y clockwise
          case 'E':
            rotatePiece('y', -90);
            break;
        }
      });

      // Optional: Reset faster fall speed on key up for ArrowDown
      // document.addEventListener('keyup', (event) => {
      //     if (event.key === 'ArrowDown' && !isGameOver) {
      //         clearInterval(gameLoopInterval);
      //         startGameLoop(); // Restart with normal speed
      //     }
      // });
    }

    function gameStep () {
      if (isGameOver) {
        clearInterval(gameLoopInterval);
        return;
      }
      if (currentPiece) {
        movePiece(0, -1, 0); // Attempt to move down
      } else {
        // Should not happen if spawnNewPiece is called correctly after freeze
        console.log("No current piece in game step");
        spawnNewPiece();
      }
    }

    function startGameLoop () {
      clearInterval(gameLoopInterval); // Clear any existing loop
      gameLoopInterval = setInterval(gameStep, fallSpeed);
    }

    function animate () {
      requestAnimationFrame(animate);
      // controls?.update(); // Update controls if enabled
      renderer.render(scene, camera);
    }

    // Handle window resize
    window.addEventListener('resize', () => {
      // Adjust based on container or window size
      const canvasWidth = 400; // Keep fixed or recalculate
      const canvasHeight = 600;
      // camera.aspect = window.innerWidth / window.innerHeight; // If using window size
      camera.aspect = canvasWidth / canvasHeight; // If using fixed canvas size
      camera.updateProjectionMatrix();
      // renderer.setSize(window.innerWidth, window.innerHeight); // If using window size
      renderer.setSize(canvasWidth, canvasHeight); // If using fixed canvas size

    });

    // Initialize the game
    init();

  </script>
</body>

</html>